"""
Tests for models/exceptions.py - Exception Hierarchy

Tests the comprehensive exception system including:
- HTTP status code assignment
- Rich error context
- to_http_exception() conversion
- Retry-After headers
- Error inheritance hierarchy
"""

import time

import pytest
from fastapi import HTTPException

from models.exceptions import (
    AIStudioError,
    # Base
    AIStudioProxyError,
    BrowserCrashedError,
    # Browser errors
    BrowserError,
    BrowserInitError,
    # Client errors
    ClientDisconnectedError,
    # Configuration errors
    ConfigurationError,
    EmptyResponseError,
    InvalidConfigError,
    InvalidModelError,
    InvalidParameterError,
    MissingConfigError,
    MissingParameterError,
    # Model errors
    ModelError,
    ModelListError,
    ModelSwitchError,
    PageNotReadyError,
    ProcessingTimeoutError,
    ProxyConnectionError,
    QueueFullError,
    QuotaExceededError,
    # Resource errors
    ResourceError,
    ResponseTimeoutError,
    SelectorNotFoundError,
    # Stream errors
    StreamError,
    StreamTimeoutError,
    # Timeout errors
    TimeoutError,
    # Upstream errors
    UpstreamError,
    # Validation errors
    ValidationError,
)

# ==================== BASE EXCEPTION TESTS ====================


def test_base_exception_basic():
    """Test basic AIStudioProxyError attributes."""
    error = AIStudioProxyError(message="Test error", req_id="test123", http_status=500)

    assert error.message == "Test error"
    assert error.req_id == "test123"
    assert error.http_status == 500
    assert error.retry_after is None
    assert isinstance(error.timestamp, float)
    assert str(error) == "[test123] Test error"


def test_base_exception_with_context():
    """Test AIStudioProxyError with custom context."""
    error = AIStudioProxyError(
        message="Error", req_id="abc", custom_field="value", another_field=123
    )

    assert error.context == {"custom_field": "value", "another_field": 123}


def test_base_exception_to_http():
    """Test conversion to HTTPException."""
    error = AIStudioProxyError(
        message="Server error", req_id="req1", http_status=503, retry_after=30
    )

    http_exc = error.to_http_exception()

    assert isinstance(http_exc, HTTPException)
    assert http_exc.status_code == 503
    assert "[req1] Server error" in http_exc.detail
    assert http_exc.headers == {"Retry-After": "30"}


def test_base_exception_repr():
    """Test __repr__ for debugging."""
    error = AIStudioProxyError(
        message="Test", req_id="id1", http_status=400, extra_data="value"
    )

    repr_str = repr(error)
    assert "AIStudioProxyError" in repr_str
    assert "message='Test'" in repr_str
    assert "req_id='id1'" in repr_str
    assert "http_status=400" in repr_str
    assert "extra_data" in repr_str


# ==================== BROWSER ERROR TESTS ====================


def test_browser_error_defaults():
    """Test BrowserError default status codes."""
    error = BrowserError("Page crashed")

    assert error.http_status == 503
    assert error.retry_after == 30


def test_page_not_ready_error():
    """Test PageNotReadyError with request ID."""
    error = PageNotReadyError("Page lost connection", req_id="req123")

    assert isinstance(error, BrowserError)
    assert error.message == "Page lost connection"
    assert error.http_status == 503


def test_browser_crashed_error():
    """Test BrowserCrashedError with default message."""
    error = BrowserCrashedError(req_id="req456")

    assert error.message == "Browser crashed unexpectedly"
    assert error.http_status == 503


def test_selector_not_found_error():
    """Test SelectorNotFoundError with selector context."""
    error = SelectorNotFoundError(selector="button#submit", req_id="req789")

    assert "button#submit" in error.message
    assert error.context["selector"] == "button#submit"


# ==================== MODEL ERROR TESTS ====================


def test_model_error_defaults():
    """Test ModelError default status code."""
    error = ModelError("Model issue")

    assert error.http_status == 422


def test_invalid_model_error_with_alternatives():
    """Test InvalidModelError with available models list."""
    error = InvalidModelError(
        model_id="gemini-invalid",
        available_models=["gemini-1.5-pro", "gemini-1.5-flash"],
        req_id="req1",
    )

    assert "gemini-invalid" in error.message
    assert "gemini-1.5-pro" in error.message
    assert error.context["model_id"] == "gemini-invalid"
    assert error.http_status == 422


def test_model_switch_error():
    """Test ModelSwitchError with source and target models."""
    error = ModelSwitchError(
        target_model="gemini-2.0", current_model="gemini-1.5-pro", req_id="req2"
    )

    assert "gemini-2.0" in error.message
    assert "gemini-1.5-pro" in error.message
    assert error.context["target_model"] == "gemini-2.0"


# ==================== CLIENT ERROR TESTS ====================


def test_client_disconnected_error_with_stage():
    """Test ClientDisconnectedError with processing stage."""
    error = ClientDisconnectedError(stage="model_switching", req_id="req3")

    assert "model_switching" in error.message
    assert error.stage == "model_switching"
    assert error.http_status == 499


def test_client_disconnected_error_no_stage():
    """Test ClientDisconnectedError without stage."""
    error = ClientDisconnectedError(req_id="req4")

    assert "Client disconnected" in error.message
    assert error.stage == ""


# ==================== VALIDATION ERROR TESTS ====================


def test_validation_error_defaults():
    """Test ValidationError default status code."""
    error = ValidationError("Invalid data")

    assert error.http_status == 400


def test_missing_parameter_error():
    """Test MissingParameterError with parameter name."""
    error = MissingParameterError(parameter="temperature", req_id="req5")

    assert "temperature" in error.message
    assert error.context["parameter"] == "temperature"


def test_invalid_parameter_error():
    """Test InvalidParameterError with value and reason."""
    error = InvalidParameterError(
        parameter="temperature",
        value=3.5,
        reason="must be between 0.0 and 2.0",
        req_id="req6",
    )

    assert "temperature" in error.message
    assert "3.5" in error.message
    assert "must be between 0.0 and 2.0" in error.message


# ==================== STREAM ERROR TESTS ====================


def test_stream_error_defaults():
    """Test StreamError default status code."""
    error = StreamError("Stream failed")

    assert error.http_status == 502


def test_proxy_connection_error():
    """Test ProxyConnectionError with proxy URL."""
    error = ProxyConnectionError(proxy_url="http://127.0.0.1:3120", req_id="req7")

    assert "127.0.0.1:3120" in error.message
    assert error.context["proxy_url"] == "http://127.0.0.1:3120"


def test_stream_timeout_error():
    """Test StreamTimeoutError with timeout duration."""
    error = StreamTimeoutError(timeout_seconds=30.0, req_id="req8")

    assert "30.0s" in error.message
    assert error.context["timeout_seconds"] == 30.0


# ==================== RESOURCE ERROR TESTS ====================


def test_resource_error_defaults():
    """Test ResourceError default status and retry."""
    error = ResourceError("Resource exhausted")

    assert error.http_status == 503
    assert error.retry_after == 60


def test_queue_full_error():
    """Test QueueFullError with queue size."""
    error = QueueFullError(queue_size=100, req_id="req9")

    assert "100" in error.message
    assert error.context["queue_size"] == 100


def test_browser_init_error():
    """Test BrowserInitError with custom message."""
    error = BrowserInitError(message="Playwright installation missing", req_id="req10")

    assert "Playwright installation missing" in error.message


# ==================== UPSTREAM ERROR TESTS ====================


def test_upstream_error_defaults():
    """Test UpstreamError default status and retry."""
    error = UpstreamError("Upstream issue")

    assert error.http_status == 502
    assert error.retry_after == 10


def test_ai_studio_error():
    """Test AIStudioError with AI Studio status code."""
    error = AIStudioError(
        error_message="Internal server error", status_code=500, req_id="req11"
    )

    assert "Internal server error" in error.message
    assert error.context["ai_studio_status"] == 500


def test_quota_exceeded_error():
    """Test QuotaExceededError with extended retry."""
    error = QuotaExceededError(req_id="req12")

    assert error.retry_after == 3600  # 1 hour
    assert "quota exceeded" in error.message.lower()


def test_empty_response_error():
    """Test EmptyResponseError default message."""
    error = EmptyResponseError(req_id="req13")

    assert "empty response" in error.message.lower()


# ==================== TIMEOUT ERROR TESTS ====================


def test_timeout_error_defaults():
    """Test TimeoutError default status code."""
    error = TimeoutError("Operation timeout")

    assert error.http_status == 504


def test_response_timeout_error():
    """Test ResponseTimeoutError with duration."""
    error = ResponseTimeoutError(timeout_seconds=300.0, req_id="req14")

    assert "300.0s" in error.message
    assert error.context["timeout_seconds"] == 300.0


def test_processing_timeout_error():
    """Test ProcessingTimeoutError without duration."""
    error = ProcessingTimeoutError(req_id="req15")

    assert "processing timeout" in error.message.lower()


# ==================== CONFIGURATION ERROR TESTS ====================


def test_configuration_error_defaults():
    """Test ConfigurationError default status code."""
    error = ConfigurationError("Config missing")

    assert error.http_status == 500


def test_missing_config_error():
    """Test MissingConfigError with config key."""
    error = MissingConfigError(config_key="API_KEY", req_id="req16")

    assert "API_KEY" in error.message
    assert error.context["config_key"] == "API_KEY"


def test_invalid_config_error():
    """Test InvalidConfigError with value and reason."""
    error = InvalidConfigError(
        config_key="PORT", value="invalid", reason="must be a number", req_id="req17"
    )

    assert "PORT" in error.message
    assert "invalid" in error.message
    assert "must be a number" in error.message


# ==================== INHERITANCE TESTS ====================


def test_error_hierarchy():
    """Test that exception hierarchy is correct."""
    # Browser errors inherit from BrowserError
    assert issubclass(PageNotReadyError, BrowserError)
    assert issubclass(SelectorNotFoundError, BrowserError)

    # All errors inherit from AIStudioProxyError
    assert issubclass(BrowserError, AIStudioProxyError)
    assert issubclass(ModelError, AIStudioProxyError)
    assert issubclass(ValidationError, AIStudioProxyError)
    assert issubclass(StreamError, AIStudioProxyError)

    # All errors inherit from Exception
    assert issubclass(AIStudioProxyError, Exception)


def test_error_catchable_by_base():
    """Test that specific errors can be caught by base class."""
    try:
        raise PageNotReadyError("Test", req_id="test")
    except BrowserError as e:
        assert isinstance(e, PageNotReadyError)
    except Exception:
        pytest.fail("Should have caught as BrowserError")


# ==================== HTTP EXCEPTION CONVERSION TESTS ====================


def test_to_http_exception_preserves_status():
    """Test that to_http_exception() uses correct HTTP status."""
    test_cases = [
        (BrowserError("Browser error"), 503),
        (ModelError("Model error"), 422),
        (ValidationError("Validation error"), 400),
        (StreamError("Stream error"), 502),
        (TimeoutError("Timeout error"), 504),
    ]

    for error, expected_status in test_cases:
        http_exc = error.to_http_exception()
        assert http_exc.status_code == expected_status


def test_to_http_exception_includes_retry_after():
    """Test that retry_after is included in headers."""
    error = ResourceError("Resource issue", retry_after=120)
    http_exc = error.to_http_exception()

    assert http_exc.headers == {"Retry-After": "120"}


def test_to_http_exception_without_retry():
    """Test that headers are None when no retry_after."""
    error = ValidationError("Bad request")
    http_exc = error.to_http_exception()

    # Should be None, not empty dict
    assert http_exc.headers is None


# ==================== CONTEXT PRESERVATION TESTS ====================


def test_context_preservation():
    """Test that custom context is preserved."""
    error = AIStudioProxyError(
        message="Error",
        req_id="test",
        custom_key="custom_value",
        another_key=42,
        nested={"a": 1, "b": 2},
    )

    assert error.context["custom_key"] == "custom_value"
    assert error.context["another_key"] == 42
    assert error.context["nested"] == {"a": 1, "b": 2}


def test_timestamp_is_recent():
    """Test that timestamp is set to current time."""
    before = time.time()
    error = AIStudioProxyError("Test")
    after = time.time()

    assert before <= error.timestamp <= after


# ==================== BACKWARD COMPATIBILITY TESTS ====================


def test_client_disconnected_error_backward_compat():
    """Test that ClientDisconnectedError works like old implementation."""
    # Old usage (just message)
    error = ClientDisconnectedError("test_stage", req_id="req1")

    assert isinstance(error, Exception)
    assert "test_stage" in error.message
    assert error.req_id == "req1"


# ==================== EDGE CASES ====================


def test_error_without_req_id():
    """Test errors work without req_id."""
    error = BrowserError("No request ID")

    assert error.req_id is None
    assert "No request ID" == error.message
    assert "[" not in str(error)  # Should not have [req_id] prefix


def test_error_with_empty_context():
    """Test error with no additional context."""
    error = AIStudioProxyError("Simple error")

    assert error.context == {}


def test_custom_http_status_override():
    """Test that custom http_status overrides default."""
    error = BrowserError("Custom status", http_status=418)  # I'm a teapot

    assert error.http_status == 418


def test_custom_retry_after_override():
    """Test that custom retry_after overrides default."""
    error = ResourceError("Custom retry", retry_after=5)

    assert error.retry_after == 5


def test_processing_timeout_error_with_duration():
    """Test ProcessingTimeoutError with timeout_seconds (covers line 427)."""
    error = ProcessingTimeoutError(timeout_seconds=30.0, req_id="req16")

    # Verify timeout is included in message (line 427)
    assert "30.0s" in error.message
    assert "processing timeout" in error.message.lower()
    assert error.context["timeout_seconds"] == 30.0


def test_model_list_error():
    """Test ModelListError initialization (covers line 196)."""
    error = ModelListError(message="Failed to parse models", req_id="req17")

    # Verify super().__init__ was called (line 196)
    assert error.message == "Failed to parse models"
    assert error.req_id == "req17"
    assert error.http_status == 422  # Inherits from ModelError
